﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using SimpleSpreadsheet.Core;
using SimpleSpreadsheet.Core.Expressions;

namespace SimpleSpreadsheet.Expressions
{
    /// <summary>
    /// Visitor that evaluates expressions
    /// </summary>
    public class ExpressionEvaluator : ExpressionVisitor
    {
        private readonly IDictionary<string, IFunction> _formulaCollection;
        private readonly NumberFormatInfo _numberFormatInfo;

        /// <summary>
        /// Creates the instance of evaluator with provided function implementations
        /// </summary>
        /// <param name="formulas"></param>
        public ExpressionEvaluator(IDictionary<string, IFunction> formulas)
        {
            References = new Dictionary<string, decimal>();
            _formulaCollection = formulas;
            _numberFormatInfo = new NumberFormatInfo();
            _numberFormatInfo.NumberDecimalSeparator = ".";
        }

        #region Implementation of base class abstract methods

        public override object Evaluate(BaseExpression expression)
        {
            expression.Accept(this);
            return Result;
        }

        public override void Visit(BaseExpression expression)
        {
            throw new Exception("The method or operation is not implemented.");
        }

        public override void Visit(BinaryExpresssion expression)
        {
            // Evaluates the left expression and saves the value
            expression.LeftExpression.Accept(this);
            object left = Result;

            // Evaluates the right expression and saves the value
            expression.RightExpression.Accept(this);
            object right = Result;

            switch (expression.Type)
            {
                case BinaryExpressionType.And:
                    Result = Convert.ToBoolean(left) && Convert.ToBoolean(right);
                    break;

                case BinaryExpressionType.Or:
                    Result = Convert.ToBoolean(left) || Convert.ToBoolean(right);
                    break;

                case BinaryExpressionType.Div:
                    Result = Convert.ToDecimal(left)/Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Equal:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                                                      Convert.ChangeType(right, left.GetType())) == 0;
                    break;

                case BinaryExpressionType.Greater:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                                                      Convert.ChangeType(right, left.GetType())) > 0;
                    break;

                case BinaryExpressionType.GreaterOrEqual:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                                                      Convert.ChangeType(right, left.GetType())) >= 0;
                    break;

                case BinaryExpressionType.Lesser:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                                                      Convert.ChangeType(right, left.GetType())) < 0;
                    break;

                case BinaryExpressionType.LesserOrEqual:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                                                      Convert.ChangeType(right, left.GetType())) <= 0;
                    break;

                case BinaryExpressionType.Minus:
                    Result = Convert.ToDecimal(left) - Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Modulo:
                    Result = Convert.ToDecimal(left)%Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.NotEqual:
                    // Use the type of the left operand to make the comparison
                    Result = Comparer.Default.Compare(left,
                                                      Convert.ChangeType(right, left.GetType())) != 0;
                    break;

                case BinaryExpressionType.Plus:
                    Result = Convert.ToDecimal(left) + Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Times:
                    Result = Convert.ToDecimal(left)*Convert.ToDecimal(right);
                    break;

                case BinaryExpressionType.Pow:
                    Result = Math.Pow(Convert.ToDouble(left), Convert.ToDouble(right));
                    break;
            }
        }

        public override void Visit(UnaryExpression expression)
        {
            // Recursively evaluates the underlying expression
            expression.Expression.Accept(this);

            switch (expression.Type)
            {
                case UnaryExpressionType.Not:
                    Result = !Convert.ToBoolean(Result);
                    break;

                case UnaryExpressionType.Negate:
                    Result = -Convert.ToDecimal(Result);
                    break;
            }
        }

        public override void Visit(ValueExpression expression)
        {
            switch (expression.ExpressionType)
            {
                case ValueExpressionType.Float:
                    Result = decimal.Parse(expression.Text, _numberFormatInfo);
                    break;

                case ValueExpressionType.Integer:
                    Result = int.Parse(expression.Text);
                    break;

                case ValueExpressionType.String:
                    Result = expression.Text;
                    break;
            }
        }

        public override void Visit(FunctionExpression functionExpression)
        {
            // Check if there is an implementation for this function
            if (!_formulaCollection.ContainsKey(functionExpression.Name))
                throw new NotSupportedException(String.Format("Function '{0}' is not supported", functionExpression.Name));

            // Get function implementation
            IFunction function = _formulaCollection[functionExpression.Name];

            Result = function.Evaluate(functionExpression.Expressions, this);
        }

        public override void Visit(ReferenceExpression referenceExpression)
        {
            if (References.ContainsKey(referenceExpression.Name))
                Result = References[referenceExpression.Name];
        }

        public override void Visit(ParameterExpression parameterExpression)
        {
            // The parameter should be defined in a call back method
            var args = new ParameterArgs();

            // Calls external implementation
            OnEvaluateParameter(parameterExpression.Name, args);

            if (args.Result == null)
                throw new ArgumentException("Parameter not defined", parameterExpression.Name);

            Result = args.Result;
        }

        #endregion

        public IDictionary<string, decimal> References { get; set; }
        public object Result { get; protected set; }
    }
}